// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
#include "pch.h"
#include "LonLatController.h"

// Adapted from the Direct3D bump mapping sample
// http://code.msdn.microsoft.com/windowsapps/Direct3D-bump-mapping-619d8d23/

using namespace Weathr;

using namespace concurrency;
using namespace DirectX;
using namespace Platform;
using namespace std;
using namespace Windows::UI::Core;
using namespace Windows::System;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Devices::Input;
using namespace Windows::ApplicationModel;
using namespace Windows::UI::Input;

const float MaxVelocity = 15.0f;        // pixels per ms
const float Acceleration = -0.125f;     // pixels per ms^2

const float MinRadius = 1.25f;          // minimum zoom amount
const float MaxRadius = 2.00f;          // maximum zoom amount

const float DampenFactor = 0.75f;       // used when the sphere bounces off the poles

const float VelocityFactor = 1.0f;      // initial factor to give the sphere a little "push"

const float HoldingDeceleration = 8.0f; // deceleration factor when the user is holding to stop the sphere

LonLatController^ LonLatController::m_current = nullptr;

// Suspends processing of input.
void LonLatController::SuspendInput()
{
    // Detach event handlers when suspend count reaches 1.
    if (++m_inputSuspendCount == 1)
    {
        m_lonLatPointerID1 = 0;
        m_lonLatPointerID2 = 0;
        m_lonLatPointerGesture = 0;
        m_pointersDown.clear();

        m_window->PointerPressed -= m_tokenPointerPressed;
        m_window->PointerMoved -= m_tokenPointerMoved;
        m_window->PointerReleased -= m_tokenPointerReleased;
        m_window->PointerWheelChanged -= m_tokenPointerWheelChanged;
        m_gestureRecognizer->ManipulationStarted::remove(m_tokenManipulationStarted);
        m_gestureRecognizer->ManipulationUpdated::remove(m_tokenManipulationUpdated);
        m_gestureRecognizer->ManipulationCompleted::remove(m_tokenManipulationCompleted);
        m_gestureRecognizer->Tapped::remove(m_tokenTapped);
    }
}

// Resumes processing of input.
void LonLatController::ResumeInput()
{
    // Attach event handlers when suspend count reaches 0.
    if (--m_inputSuspendCount == 0)
    {
        auto window = m_window;
        auto gestureRecognizer = m_gestureRecognizer;

        m_tokenPointerPressed = window->PointerPressed +=
            ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(m_current, &LonLatController::OnPointerPressed);

        m_tokenPointerMoved = window->PointerMoved +=
            ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(m_current, &LonLatController::OnPointerMoved);

        m_tokenPointerReleased = window->PointerReleased +=
            ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(m_current, &LonLatController::OnPointerReleased);

        m_tokenPointerWheelChanged = window->PointerWheelChanged +=
            ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(m_current, &LonLatController::OnPointerWheelChanged);

        m_tokenManipulationStarted = gestureRecognizer->ManipulationStarted::add(
            ref new Windows::Foundation::TypedEventHandler<
            GestureRecognizer^, ManipulationStartedEventArgs^>(
            m_current, &LonLatController::OnManipulationStarted));

        m_tokenManipulationUpdated = gestureRecognizer->ManipulationUpdated::add(
            ref new Windows::Foundation::TypedEventHandler<
            GestureRecognizer^, ManipulationUpdatedEventArgs^>(
            m_current, &LonLatController::OnManipulationUpdated));

        m_tokenManipulationCompleted = gestureRecognizer->ManipulationCompleted::add(
            ref new Windows::Foundation::TypedEventHandler<GestureRecognizer^,
            ManipulationCompletedEventArgs^>(m_current, &LonLatController::OnManipulationCompleted));

        m_tokenTapped = gestureRecognizer->Tapped::add(
            ref new Windows::Foundation::TypedEventHandler<GestureRecognizer^,
            TappedEventArgs^>(m_current, &LonLatController::OnTapped));
    }
    assert(m_inputSuspendCount >= 0);
}

// Begins sphere animation to the provided latitude and longitude.
task<void> LonLatController::AnimateToLatLonAsync(float latitude, float longitude)
{
    m_animationEvent = task_completion_event<void>();

    // Set up animation.
    const XMFLOAT3 pos = get_Position();
    const LatLon cameraLatlon = LatLon::FromSphereCoordinates(XMVectorSet(pos.x, pos.y, pos.z, 0.0));

    const LatLon destinationLatLon(latitude, longitude);

    const double distance = DistanceInFeet(cameraLatlon, destinationLatLon);

    XMVECTOR cameraPosition = XMVector3Normalize(cameraLatlon.ToSphereCoordinates(1.0f));
    XMVECTOR destinationPosition = XMVector3Normalize(destinationLatLon.ToSphereCoordinates(1.0f));

    // Angle between vectors cannot be 0 or PI. However, there isn't likely to be a location at either pole.
    const XMVECTOR up = XMVectorSet(0, 1, 0, 0);

    XMVectorSetW(cameraPosition, XMVectorGetX(XMVector3Dot(cameraPosition, up)));
    cameraPosition = XMVector4Normalize(cameraPosition);
    XMStoreFloat4(&m_endRotation, cameraPosition);

    XMVectorSetW(destinationPosition, XMVectorGetX(XMVector3Dot(destinationPosition, up)));
    destinationPosition = XMVector4Normalize(destinationPosition);
    XMStoreFloat4(&m_startRotation, destinationPosition);

    const float angle = XMVectorGetX(XMVector4AngleBetweenVectors(destinationPosition, cameraPosition));
    if (angle < FLT_EPSILON)
    {
        m_animationEvent.set();
        return create_task(m_animationEvent);
    }

    // Compute the animation time.
    // Set up a maximum time to rotate around the entire Earth and scale back from there.

    float32 EarthCircumferenceInFeet = 131480184.f; // Earth's circumference in feet at the equator.
    float32 MaxRotationTime = 6.0f;                 // Rotation time for distance = EarthCircumferenceInFeet

    m_rotationMaxTime = static_cast<float32>(distance) * MaxRotationTime / EarthCircumferenceInFeet;

    m_rotationTimeRemaining = m_rotationMaxTime;

    // If the angle is too small, just set the event now.
    if (m_rotationTimeRemaining < FLT_EPSILON)
    {
        m_animationEvent.set();
    }
    return create_task(m_animationEvent);
}

void LonLatController::CancelLatLonAnimation(bool moveToEnd)
{
    m_pointerVelocity = { 0.0f, 0.0f };

    if (m_rotationTimeRemaining <= FLT_EPSILON)
    {
        return;
    }

    if (moveToEnd)
    {
        XMVECTOR c = XMQuaternionSlerp(XMLoadFloat4(&m_startRotation), XMLoadFloat4(&m_endRotation), m_rotationMaxTime) * m_radius;
        XMFLOAT3 pos;
        XMStoreFloat3(&pos, c);
        SetPosition(pos);
    }
    m_rotationTimeRemaining = 0.0f;
    m_animationEvent.set();

    // Clear movement input accumulator for use during next frame.
    m_lonLatCommand = XMFLOAT3(0.0f, 0.0f, 0.0f);
}

// accessor to set position of controller
void LonLatController::SetPosition(_In_ XMFLOAT3 pos)
{
    m_longitude = atan2f(pos.x, pos.z);
    float r = hypotf(pos.x, pos.z);
    m_latitude = atan2f(pos.y, r);
}

// accessor to set position of controller
void LonLatController::SetOrientation(_In_ float latitude, _In_ float longitude)
{
    m_latitude = latitude;
    m_longitude = longitude;
}

// returns the position of the controller object
XMFLOAT3 LonLatController::get_Position()
{
    float y = m_radius*sinf(m_latitude);    // up-down
    float r = m_radius*cosf(m_latitude);    // in the plane
    float z = r*cosf(m_longitude);          // fwd-back
    float x = r*sinf(m_longitude);          // left-right

    return{ x, y, z };
}

// returns the point at which the camera controller is facing
XMFLOAT3 LonLatController::get_LookPoint()
{
    return{ 0, 0, 0 };
}

void LonLatController::Update()
{
    // When in animation mode, update the animation and return (ignore user input).
    if (m_rotationTimeRemaining >= FLT_EPSILON)
    {
        m_animationTimer->Update();
        m_rotationTimeRemaining -= m_animationTimer->Total / 1000.0f;
        m_rotationTimeRemaining = max(0.0f, m_rotationTimeRemaining); // clamp to 0

        const float t = m_rotationTimeRemaining / m_rotationMaxTime;
        XMVECTOR c = XMQuaternionSlerp(XMLoadFloat4(&m_startRotation), XMLoadFloat4(&m_endRotation), t) * m_radius;
        XMFLOAT3 pos;
        XMStoreFloat3(&pos, c);
        SetPosition(pos);

        // Set the completion event when the animation has finished.
        if (m_rotationTimeRemaining < FLT_EPSILON)
        {
            m_animationEvent.set();
        }

        // Clear movement input accumulator for use during next frame.
        m_lonLatCommand = XMFLOAT3(0.0f, 0.0f, 0.0f);

        return;
    }

    // Return if we're not processing input.
    if (m_inputSuspendCount != 0)
    {
        return;
    }

    // Process animation
    if (fabs(m_pointerVelocity.X) > FLT_EPSILON || fabs(m_pointerVelocity.Y) > FLT_EPSILON)
    {
        m_velocityTimer->Update();

        float timeDelta = m_velocityTimer->Delta;
        XMFLOAT2 rotationDelta(timeDelta * m_pointerVelocity.X, timeDelta * m_pointerVelocity.Y);

        const auto maxDelta = MaxVelocity * timeDelta;
        rotationDelta.x = min(rotationDelta.x, +maxDelta);
        rotationDelta.x = max(rotationDelta.x, -maxDelta);
        rotationDelta.y = min(rotationDelta.y, +maxDelta);
        rotationDelta.y = max(rotationDelta.y, -maxDelta);

        ProcessRotateCommand(rotationDelta);

        // If the user is holding, increase the rate of deceleration.
        float accelerationFactor = Acceleration;
        if (!m_pointersDown.empty())
        {
            accelerationFactor *= HoldingDeceleration;
        }

        const auto previousVelocity = m_pointerVelocity;
        if (m_pointerVelocity.X != 0.0f)
        {
            // Multiply acceleration by magnitude.
            const auto acceleration = accelerationFactor * (m_pointerVelocity.X / abs(m_pointerVelocity.X));

            m_pointerVelocity.X = m_pointerVelocity.X + (acceleration * m_velocityTimer->Total);
        }
        if (m_pointerVelocity.Y != 0.0f)
        {
            // Multiply acceleration by magnitude.
            const auto acceleration = accelerationFactor * (m_pointerVelocity.Y / abs(m_pointerVelocity.Y));

            m_pointerVelocity.Y = m_pointerVelocity.Y + (acceleration * m_velocityTimer->Total);
        }

        // Stop if we've become significantly slow or switched signs.
        if (abs(m_pointerVelocity.X) < FLT_EPSILON || (previousVelocity.X > 0.0f) != (m_pointerVelocity.X > 0.0f))
        {
            m_pointerVelocity.X = 0.0f;
        }
        if (abs(m_pointerVelocity.Y) < FLT_EPSILON || (previousVelocity.Y > 0.0f) != (m_pointerVelocity.Y > 0.0f))
        {
            m_pointerVelocity.Y = 0.0f;
        }
    }

    // poll keyboard state
    Windows::UI::Core::CoreVirtualKeyStates KeyState;

    // Arrow keys
    KeyState = m_window->GetAsyncKeyState(VirtualKey::Up);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.z += 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::Down);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.z -= 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::Left);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.x -= 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::Right);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.x += 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::PageUp);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.y += 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::PageDown);
    if (bool(KeyState == CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.y -= 1.0f;
    }

    // WASD keys
    KeyState = m_window->GetAsyncKeyState(VirtualKey::W);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.y += 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::A);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.x -= 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::S);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.y -= 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::D);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.x += 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::Space);
    if (bool(KeyState & CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.z += 1.0f;
    }

    KeyState = m_window->GetAsyncKeyState(VirtualKey::X);
    if (bool(KeyState == CoreVirtualKeyStates::Down))
    {
        m_lonLatCommand.z -= 1.0f;
    }

    // scale for sensitivity adjustment
    XMFLOAT3 command;
    command.x = m_lonLatCommand.x * LONLATKEY_GAIN;
    command.y = m_lonLatCommand.y * LONLATKEY_GAIN;
    command.z = m_lonLatCommand.z * LONLATKEY_GAIN;
    float altitude = m_radius - 1;
    command.x = command.x * altitude * 0.5f;    // logarithmic gain towards unit sphere
    command.y = command.y * altitude * 0.5f;
    command.z = command.z * altitude * 0.5f;

    // update state with command input
    m_latitude += command.z;
    m_longitude += command.x;
    if (!m_animateZoom)
    {
        m_radius -= command.y;           // fwd stick is down towards object
    }

    // Limit latitude to poles
    float limit = PI_F / 2 - 0.01f;
    if (m_latitude < -limit || m_latitude > +limit)
    {
        m_latitude = __max(-limit, m_latitude);
        m_latitude = __min(+limit, m_latitude);

        // negate and dampen the velocity
        m_pointerVelocity.Y = -m_pointerVelocity.Y * DampenFactor;
    }

    // keep longitude in sane range by wrapping
    if (m_longitude > PI_F)
    {
        m_longitude -= PI_F * 2;
    }
    else if (m_longitude < -PI_F)
    {
        m_longitude += PI_F * 2;
    }

    // process zoom animation
    if (m_animateZoom)
    {
        m_zoomAnimationTimer->Update();

        // Interpolate between initial and target zoom over 1/10 second.
        // V0 + t * (V1 - V0)
        m_radius = m_zoomInitial + (m_zoomAnimationTimer->Total * 10.0f * (m_zoomTarget - m_zoomInitial));

        if (m_radius <= MinRadius || m_radius >= MaxRadius)
        {
            m_animateZoom = false;
        }
    }

    // clamp radius
    m_radius = __max(MinRadius, m_radius);
    m_radius = __min(MaxRadius, m_radius);

    // clear movement input accumulator for use during next frame
    m_lonLatCommand = XMFLOAT3(0.0f, 0.0f, 0.0f);
}

void LonLatController::SaveInternalState(_In_ IMap<String^, Object^>^ state)
{
    if (state->HasKey("m_longitude"))
    {
        state->Remove("m_longitude");
    }
    if (state->HasKey("m_latitude"))
    {
        state->Remove("m_latitude");
    }
    if (state->HasKey("m_radius"))
    {
        state->Remove("m_radius");
    }
    state->Insert("m_longitude", PropertyValue::CreateSingle(m_longitude));
    state->Insert("m_latitude", PropertyValue::CreateSingle(m_latitude));
    state->Insert("m_radius", PropertyValue::CreateSingle(m_radius));
}

void LonLatController::LoadInternalState(_In_ IMap<String^, Object^>^ state)
{
    if (state->HasKey("m_longitude") && state->HasKey("m_latitude") && state->HasKey("m_radius"))
    {
        m_longitude = safe_cast<IPropertyValue^>(state->Lookup("m_longitude"))->GetSingle();
        m_latitude = safe_cast<IPropertyValue^>(state->Lookup("m_latitude"))->GetSingle();
        m_radius = safe_cast<IPropertyValue^>(state->Lookup("m_radius"))->GetSingle();
    }
}

LonLatController::LonLatController() 
: m_radius(1.5f)
, m_lonLatInUse(false)
, m_lonLatPointerID1(0)
, m_lonLatPointerID2(0)
, m_lonLatPointerGesture(0)
, m_window(CoreWindow::GetForCurrentThread())
, m_gestureRecognizer(ref new GestureRecognizer())
, m_pointerVelocity(0, 0)
, m_velocityTimer(ref new BasicTimer())
, m_zoomAnimationTimer(ref new BasicTimer())
, m_animateZoom(false)
, m_inputSuspendCount(1)
, m_animationTimer(ref new BasicTimer())
, m_rotationTimeRemaining(0.0f)
{
    // Configure gesture recognizer
    m_gestureRecognizer->GestureSettings = GestureSettings::ManipulationTranslateX | GestureSettings::ManipulationTranslateY | GestureSettings::DoubleTap;
}

void LonLatController::ProcessRotateCommand(const XMFLOAT2& rotationDelta)
{
    // update our orientation based on the command
    float altitude = m_radius - 1;

    m_latitude += rotationDelta.y *altitude*0.5f;
    m_longitude -= rotationDelta.x *altitude*0.5f;

    // Limit lattitude to poles
    float limit = PI_F / 2 - 0.01f;
    if (m_latitude < -limit || m_latitude > +limit)
    {
        m_latitude = __max(-limit, m_latitude);
        m_latitude = __min(+limit, m_latitude);

        // negate and dampen the velocity
        m_pointerVelocity.Y = -m_pointerVelocity.Y * DampenFactor;
    }

    // keep longitude in sane range by wrapping
    if (m_longitude > PI_F)
    {
        m_longitude -= PI_F * 2;
    }
    else if (m_longitude < -PI_F)
    {
        m_longitude += PI_F * 2;
    }

    // clamp radius
    m_radius = __max(MinRadius, m_radius);
    m_radius = __min(MaxRadius, m_radius);
}

// method for handling touch press events or mouse clicks
void LonLatController::OnPointerPressed(_In_ CoreWindow^ sender, _In_ PointerEventArgs^ args)
{
    XMFLOAT2 position(  // position of contact
        args->CurrentPoint->Position.X,
        args->CurrentPoint->Position.Y
        );

    uint32 pointerID = args->CurrentPoint->PointerId;       // ID of pointer that pressed
    if (m_pointersDown.find(pointerID) != end(m_pointersDown))
    {
        m_pointersDown.erase(pointerID);
    }
    m_pointersDown.insert(std::make_pair(pointerID, position));

    if (m_pointersDown.size() == 1)                     // if this is first pointer down
    {
        m_lonLatLastPoint1 = position;                  // save for computing relative delta
        m_lonLatPointerID1 = pointerID;

        // Pass to gesture recognizer only when first pointer is down.
        PointerPoint^ pointerPoint = PointerPoint::GetCurrentPoint(pointerID);
        try
        {
            m_gestureRecognizer->ProcessDownEvent(pointerPoint);
        }
        catch (InvalidArgumentException^ e)
        {
            // TODO: I get this exception a lot, such as when you manipulate the command bar and then the sphere. Investigate this.
        }

        m_lonLatPointerGesture = pointerID;
    }
    else if (m_pointersDown.size() == 2)                // if this is the second
    {
        m_lonLatLastPoint2 = position;                  // save for computing relative delta
        m_lonLatPointerID2 = pointerID;
        XMFLOAT2 del;
        del.x = m_lonLatLastPoint2.x - m_lonLatLastPoint1.x;
        del.y = m_lonLatLastPoint2.y - m_lonLatLastPoint1.y;
        m_lastDistance = hypotf(del.x, del.y);
    }
    // else //  ignore any 3rd or more pointers
}

void LonLatController::OnPointerMoved(_In_ CoreWindow^ sender, _In_ PointerEventArgs^ args)
{
    if (args->CurrentPoint->PointerDevice->PointerDeviceType == PointerDeviceType::Mouse)
    {
        if (!args->CurrentPoint->Properties->IsLeftButtonPressed)
        {
            return;
        }
    }

    XMFLOAT2 position(
        args->CurrentPoint->Position.X,
        args->CurrentPoint->Position.Y
        );

    uint32 pointerID = args->CurrentPoint->PointerId;       // ID of pointer that moved
    auto pointerIt = m_pointersDown.find(pointerID);
    if (pointerIt == end(m_pointersDown))
    {
        return;
    }
    pointerIt->second = position;

    // Pass to gesture recognizer only when the gesture pointer is down.
    if (pointerID == m_lonLatPointerGesture)
    {
        IVector<PointerPoint^>^ pointerPoints = PointerPoint::GetIntermediatePoints(args->CurrentPoint->PointerId);
        m_gestureRecognizer->ProcessMoveEvents(pointerPoints);
    }

    if (m_pointersDown.size() == 1)   // then drag/rotate
    {
        XMFLOAT2 pointerDelta;
        pointerDelta.x = position.x - m_lonLatLastPoint1.x; // how far did pointer move
        pointerDelta.y = position.y - m_lonLatLastPoint1.y;

        XMFLOAT2 rotationDelta;
        rotationDelta.x = pointerDelta.x * LONLAT_GAIN;     // scale for control sensitivity
        rotationDelta.y = pointerDelta.y * LONLAT_GAIN;
        m_lonLatLastPoint1 = position;                      // save for next time through

        ProcessRotateCommand(rotationDelta);
    }
    else if (m_pointersDown.size() >= 2)       // pinch/zoom mode
    {
        if (pointerID == m_lonLatPointerID1)
        {
            m_lonLatLastPoint1 = position;                  // save for computing relative delta
        }
        else if (pointerID == m_lonLatPointerID2)
        {
            m_lonLatLastPoint2 = position;                  // save for computing relative delta
        }
        XMFLOAT2 del;
        del.x = m_lonLatLastPoint2.x - m_lonLatLastPoint1.x;
        del.y = m_lonLatLastPoint2.y - m_lonLatLastPoint1.y;
        float distance = hypotf(del.x, del.y);

        float delta = distance - m_lastDistance;
        m_lonLatCommand.y += delta*0.1f;                    // zoom in/out
        m_lastDistance = distance;                          // save for next frame
    }
}

void LonLatController::OnPointerReleased(_In_ CoreWindow^ sender, _In_ PointerEventArgs^ args)
{
    uint32 pointerID = args->CurrentPoint->PointerId;       // ID of pointer that released
    auto pointerIt = m_pointersDown.find(pointerID);
    if (pointerIt == end(m_pointersDown))
    {
        // May have been a pointer from another screen. For example, this can happen when navigating to a page
        // that enables or disables input.
        return;
    }
    m_pointersDown.erase(pointerIt);

    if (pointerID == m_lonLatPointerID1)    // this was the first down pointer
    {
        m_lonLatPointerID1 = m_lonLatPointerID2;                // use remaining contact as rotate, not zoom
        m_lonLatLastPoint1 = m_lonLatLastPoint2;
    }

    // Pass to gesture recognizer only when first pointer is released.
    if (pointerID == m_lonLatPointerGesture)
    {
        PointerPoint^ pointerPoint = PointerPoint::GetCurrentPoint(pointerID);
        try
        {
            m_gestureRecognizer->ProcessUpEvent(pointerPoint);
        }
        catch (...)
        {
            // TODO: I get this exception a lot, such as when you manipulate the command bar and then the sphere. Investigate this.
        }
        m_gestureRecognizer->CompleteGesture();
        m_lonLatPointerGesture = 0;
    }
}

void LonLatController::OnPointerWheelChanged(_In_ CoreWindow^ sender, _In_ PointerEventArgs^ args)
{
    int wheelDelta = args->CurrentPoint->Properties->MouseWheelDelta;
    m_lonLatCommand.y += wheelDelta*0.1f;      // zoom in/out
}

void LonLatController::OnPointerCaptureLost(_In_ CoreWindow^ sender, _In_ PointerEventArgs^ args)
{
}

void LonLatController::OnManipulationStarted(_In_ GestureRecognizer^ sender, _In_ ManipulationStartedEventArgs^ args)
{
}

void LonLatController::OnManipulationUpdated(_In_ GestureRecognizer^ sender, _In_ ManipulationUpdatedEventArgs^ args)
{
}

void LonLatController::OnManipulationCompleted(_In_ GestureRecognizer^ sender, _In_ ManipulationCompletedEventArgs^ args)
{
    // If velocity is low, the user is likely repositioning the sphere, rather than wanting it to spin on its own.
    if (abs(args->Velocities.Linear.X) > .005f || abs(args->Velocities.Linear.Y) > .005f)
    {
        m_pointerVelocity.X += VelocityFactor * args->Velocities.Linear.X;
        m_pointerVelocity.Y += VelocityFactor * args->Velocities.Linear.Y;
        m_velocityTimer->Reset();
    }
}

void LonLatController::OnTapped(_In_ Windows::UI::Input::GestureRecognizer^ sender, _In_ Windows::UI::Input::TappedEventArgs^ args)
{
    // If user double taps, zoom either all the way in or all the way out out, depending on whichever is closer.
    if (args->TapCount == 2u)
    {
        const auto minDist = abs(m_radius - MinRadius);   // distance to min zoom
        const auto maxDist = abs(m_radius - MaxRadius);   // distance to max zoom
        m_zoomInitial = m_radius;
        m_zoomTarget = (minDist > maxDist) ? MinRadius : MaxRadius;
        m_animateZoom = true;
        m_zoomAnimationTimer->Reset();
    }
}
